/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package com.cognition.util.marshal;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * A byte array input stream that can unmarshal java primitive (and String)
 * types.  This input stream will set the mark for its own use.
 *
 * <p>The stream is expected to be in the format of :
 * type (byte), size (int), data, type(byte), etc.
 *
 * @author richard
 */
public class JavaPrimitiveInputStream extends ByteArrayInputStream{
    public JavaPrimitiveInputStream(byte[] b){
        super(b);
    }

    public JavaPrimitiveInputStream(byte[] b, int offset, int len){
        super(b, offset, len);
    }

    /**
     * Peeks at the next type in the stream.  Returns a -1 if the type is
     * not valid or cannot be determined.  
     * @return the next type to check against values in MarshalTypes.
     */
    public int getNextType(){
        super.mark(1);
        int result = super.read();
        super.reset();
        return result;

    }

    public int read(){
        throw new UnsupportedOperationException(
                "You must use marshal operations to get data from this stream.");
    }

    public int read(byte[] b, int off, int len){
        throw new UnsupportedOperationException(
                "You must use marshal operations to get data from this stream.");
    }

    public void mark(int readAheadLimit){
        throw new UnsupportedOperationException(
                "You must use marshal operations to get data from this stream.");
    }

    public void reset(){
        throw new UnsupportedOperationException(
                "You must use marshal operations to get data from this stream.");
    }

    public long skip(long n){
        throw new UnsupportedOperationException(
                "You must use marshal operations to get data from this stream.");
    }

    /**
     * Checks a stream to see if it is constructed correctly.
     * The stream will be considered constructed correctly if all the types are
     * correct, and there are no other errors.  You must reset the buffer
     * after calling this method.
     *
     * @return true if the buffer contains data that can be unmarshalled
     */
    public static boolean isValid(ByteArrayInputStream is) {
        
        if (is.available() <= 2)
            // should always have a minimum of 2 bytes -
            // 1 for type, 1 for data
            return false;

        try{
            while(is.available() > 0){
                // check the type
                int i = is.read();
                if (i < 0)
                    return false;

                boolean skipped = true;

                switch(i){
                    case MarshalTypes.TYPE_BOOLEAN:
                        is.skip(BooleanMarshaller.MARSHAL_SIZE);
                        break;

                    case MarshalTypes.TYPE_BOOLEAN_ARRAY:
                        skipped = skipLength(is);
                        if (!skipped)
                            return false;
                        break;

                    case MarshalTypes.TYPE_BYTE:
                        is.skip(ByteMarshaller.MARSHAL_SIZE);
                        break;

                    case MarshalTypes.TYPE_BYTE_ARRAY:
                        skipped = skipLength(is);
                        if (!skipped)
                            return false;
                        break;

                    case MarshalTypes.TYPE_CHAR:
                        is.skip(CharMarshaller.MARSHAL_SIZE);
                        break;

                    case MarshalTypes.TYPE_CHAR_ARRAY:
                        skipped = skipLength(is);
                        if (!skipped)
                            return false;
                        break;

                    case MarshalTypes.TYPE_DOUBLE:
                        is.skip(DoubleMarshaller.MARSHAL_SIZE);
                        break;

                    case MarshalTypes.TYPE_DOUBLE_ARRAY:
                        skipped = skipLength(is);
                        if (!skipped)
                            return false;
                        break;

                    case MarshalTypes.TYPE_FLOAT:
                        is.skip(FloatMarshaller.MARSHAL_SIZE);
                        break;

                    case MarshalTypes.TYPE_FLOAT_ARRAY:
                        skipped = skipLength(is);
                        if (!skipped)
                            return false;
                        break;

                    case MarshalTypes.TYPE_INT:
                        is.skip(IntegerMarshaller.MARSHAL_SIZE);
                        break;

                    case MarshalTypes.TYPE_INT_ARRAY:
                        skipped = skipLength(is);
                        if (!skipped)
                            return false;
                        break;

                    case MarshalTypes.TYPE_LONG:
                        is.skip(LongMarshaller.MARSHAL_SIZE);
                        break;

                    case MarshalTypes.TYPE_LONG_ARRAY:
                        skipped = skipLength(is);
                        if (!skipped)
                            return false;
                        break;

                    case MarshalTypes.TYPE_SHORT:
                        is.skip(ShortMarshaller.MARSHAL_SIZE);
                        break;

                    case MarshalTypes.TYPE_SHORT_ARRAY:
                        skipped = skipLength(is);
                        if (!skipped)
                            return false;
                        break;

                    case MarshalTypes.TYPE_STRING:
                    case MarshalTypes.TYPE_STRING_ARRAY:
                        skipped = skipLength(is);
                        if (!skipped)
                            return false;
                        break;

                    default:
                        return false;
                }


            }

        } catch (IllegalAccessException ex){

            return false;
        
        }


        return true;

    }
    
    /**
     * Skips to the next field. Assumes buffer is pointed at a size field.
     * @return true if successful or false if something went wrong.
     */
    private static boolean skipLength(ByteArrayInputStream is) throws
        IllegalAccessException
    {
        
            int i = getLength(is);
            // skip to the next item
            long skipped = is.skip(i);
            if (skipped != i)
                return false;

            return true;
    }

    /**
     * Returns the size of the next data field. Blindly assumes that the
     * buffer is pointed at a length item.
     *
     */
     public static int getLength(ByteArrayInputStream is)
             throws IllegalAccessException{

     
            if (is.available() < 4)
                throw new IllegalAccessException("unable to get length.");

            // read the size - an integer
            byte[] size = new byte[4];
            int i = 0;

            for (int z = 0; z < size.length; z++){
                i = is.read();
                if (i < 0)
                    throw new IllegalAccessException("unable to get length");
                size[z] = (byte)i;
            }

            return IntegerMarshaller.unmarshal(size);

     }

    /**
     * Dumps the contents of the array.  Useful for debugging.
     */
    public static void dumpContents(ByteArrayInputStream is){

        System.out.println("========== Java Primitive Input Stream ==========");

        if (is.available() <= 2){
            System.out.println("no values found");
            // should always have a minimum of 6 bytes -
            // 1 for type and 1 for data
            return;
        }

        try{
            while(is.available() > 0){
                // check the type
                int i = is.read();
                if (i < 0){
                    System.out.println("unable to read array: to short");
                    return;
                }
                boolean skipped = true;

                switch(i){
                    case MarshalTypes.TYPE_BOOLEAN:
                        System.out.println("BOOLEAN");
                        is.skip(BooleanMarshaller.MARSHAL_SIZE);
                        break;

                    case MarshalTypes.TYPE_BOOLEAN_ARRAY:
                        System.out.println("BOOLEAN ARRAY");
                        skipped = skipLength(is);
                        if (!skipped){
                            System.out.println("Error reading length.");
                            return;
                        }
                        break;

                    case MarshalTypes.TYPE_BYTE:
                        System.out.println("BYTE");
                        is.skip(ByteMarshaller.MARSHAL_SIZE);
                        break;

                    case MarshalTypes.TYPE_BYTE_ARRAY:
                        System.out.println("BYTE ARRAY");
                        skipped = skipLength(is);
                        if (!skipped){
                            System.out.println("Error reading length.");
                            return;
                        }
                        break;

                    case MarshalTypes.TYPE_CHAR:
                        System.out.println("CHAR");
                        is.skip(CharMarshaller.MARSHAL_SIZE);
                        break;

                    case MarshalTypes.TYPE_CHAR_ARRAY:
                        System.out.println("CHAR ARRAY");
                        skipped = skipLength(is);
                        if (!skipped){
                            System.out.println("Error reading length.");
                            return;
                        }
                        break;

                    case MarshalTypes.TYPE_DOUBLE:
                        System.out.println("DOUBLE");
                        is.skip(DoubleMarshaller.MARSHAL_SIZE);
                        break;

                    case MarshalTypes.TYPE_DOUBLE_ARRAY:
                        System.out.println("DOUBLE ARRAY");
                        skipped = skipLength(is);
                        if (!skipped){
                            System.out.println("Error reading length.");
                            return;
                        }
                        break;

                    case MarshalTypes.TYPE_FLOAT:
                        System.out.println("FLOAT");
                        is.skip(FloatMarshaller.MARSHAL_SIZE);
                        break;

                    case MarshalTypes.TYPE_FLOAT_ARRAY:
                        System.out.println("FLOAT ARRAY");
                        skipped = skipLength(is);
                        if (!skipped){
                            System.out.println("Error reading length.");
                            return;
                        }
                        break;

                    case MarshalTypes.TYPE_INT:
                        System.out.println("INT");
                        is.skip(IntegerMarshaller.MARSHAL_SIZE);
                        break;

                    case MarshalTypes.TYPE_INT_ARRAY:
                        System.out.println("INT ARRAY");
                        skipped = skipLength(is);
                        if (!skipped){
                            System.out.println("Error reading length.");
                            return;
                        }
                        break;

                    case MarshalTypes.TYPE_LONG:
                        System.out.println("LONG");
                        is.skip(LongMarshaller.MARSHAL_SIZE);
                        break;

                    case MarshalTypes.TYPE_LONG_ARRAY:
                        System.out.println("LONG ARRAY");
                        skipped = skipLength(is);
                        if (!skipped){
                            System.out.println("Error reading length.");
                            return;
                        }
                        break;

                    case MarshalTypes.TYPE_SHORT:
                        System.out.println("SHORT");
                        is.skip(ShortMarshaller.MARSHAL_SIZE);
                        break;

                    case MarshalTypes.TYPE_SHORT_ARRAY:
                        System.out.println("SHORT ARRAY");
                        skipped = skipLength(is);
                        if (!skipped){
                            System.out.println("Error reading length.");
                            return;
                        }
                        break;

                    case MarshalTypes.TYPE_STRING:
                        System.out.println("STRING");
                        skipped = skipLength(is);
                        if (!skipped){
                            System.out.println("Error reading length.");
                            return;
                        }
                        break;
                    case MarshalTypes.TYPE_STRING_ARRAY:
                        System.out.println("STRING ARRAY");
                        skipped = skipLength(is);
                        if (!skipped){
                            System.out.println("Error reading length.");
                            return;
                        }
                        break;

                    default:
                        System.out.println("UNKNOWN");
                }
            }
        } catch (IllegalAccessException ex){
            System.out.println("Unable to dump contents due to error.");
            ex.printStackTrace();
        }
        System.out.println("=================================================");

    }

    /**
     * Reads items as if they were a specified type with given length/type.
     * @param t the type to read
     * @param tLen the length in eachs type of the marshalled size of the type
     * @return bytes of the next type that can be converted
     * @throws java.lang.IllegalAccessException if the bytes cannot be read.
     */
    private byte[][] readItems(int t, int tLen) throws
        IllegalAccessException{

        if (tLen <= 0)
            throw new IllegalAccessException("tLen must be > 0");
        
        // bound type
        int type = t < 0 || t >= MarshalTypes.TYPE_NAMES.length ?
            0 : t;


        super.mark(super.available());

        if (super.available() < tLen + 1)
            throw new IllegalAccessException(
                    "not enough data left to read field of type " +
                    MarshalTypes.TYPE_NAMES[type]);

        int rType = super.read();
        if (rType < 0 || rType >= MarshalTypes.TYPE_NAMES.length)
            rType = 0;

        if (rType != t){
            System.out.println("Warning: unmarshalling type " +
                    MarshalTypes.TYPE_NAMES[rType] + " as a " +
                    MarshalTypes.TYPE_NAMES[type]);
        }

        // get the length if it's there
        int length = 0;
        if (MarshalTypes.TYPE_LENGTHS_REQUIRED[type]){
            try{
                length = getLength(this);
            } catch (IllegalAccessException ex){
                super.reset();
                throw ex;
            }
        }

        if (length == 0)
            length = tLen;

        if (length%tLen != 0){
            super.reset();
            throw new IllegalAccessException("unable to read integer array.");
        }

        int itemsToRead = length / tLen;
        byte[][] result = new byte[itemsToRead][tLen];
        byte[] readBytes = new byte[tLen];

        if (itemsToRead > 0){

            try{
                for (int y = 0; y < itemsToRead; y++){
                    int val = super.read(readBytes);
                    if (val < 0){
                        super.reset();
                        throw new IllegalAccessException(
                                "error reading integer.");
                    }
                    result[y] = readBytes;


                }
            } catch (IOException ex){
                super.reset();
                throw new IllegalAccessException("error reading integer");
            }
        }

        return result;

    }


    /**
     * Reads the entire byte set of a particular item.  If the item type
     * does not reuire a length, then an exception is thrown
     * @param t the type to read
     * @return bytes of the next type that can be converted
     * @throws java.lang.IllegalAccessException if the bytes cannot be read.
     */
    private byte[] readItem(int t) throws
        IllegalAccessException{

        // bound type
        int type = t < 0 || t >= MarshalTypes.TYPE_NAMES.length ?
            0 : t;

        if (!MarshalTypes.TYPE_LENGTHS_REQUIRED[type])
            throw new IllegalAccessException("type must be variable length.");
        
        super.mark(super.available());


        int rType = super.read();
        if (rType < 0 || rType >= MarshalTypes.TYPE_NAMES.length)
            rType = 0;

        if (rType != t){
            System.out.println("Warning: unmarshalling type " +
                    MarshalTypes.TYPE_NAMES[rType] + " as a " +
                    MarshalTypes.TYPE_NAMES[type]);
        }

        // get the length if it's there
        int length = 0;
        try{
            length = getLength(this);
        } catch (IllegalAccessException ex){
            super.reset();
            throw ex;
        }

        byte[] result = new byte[length];
        try{
            super.read(result);
        } catch (IOException ex){
            super.reset();
            throw new IllegalAccessException(ex.getMessage());
        }
        
        return result;

    }

    /**
     *
     * Reads the next value as a byte, forsaking the actual type.  The marker
     * @return the next value as a byte.
     */
    public byte unMarshalByte() throws IllegalAccessException{
        byte[][] items = readItems(MarshalTypes.TYPE_BYTE,
                ByteMarshaller.MARSHAL_SIZE);

        if (items == null || items.length == 0)
            throw new IllegalAccessException("no bytes were read");

        if (items[0] == null || items[0].length == 0)
            throw new IllegalAccessException("no bytes were read");

        return items[0][0];

    }

    /**
     * Unmarshalls the next field as a byte array.
     * @return the byte array of the next field.
     */
    public byte[] unMarshalByteArray() throws IllegalAccessException{
        byte[][] items = readItems(MarshalTypes.TYPE_BYTE,
                ByteMarshaller.MARSHAL_SIZE);

        if (items == null || items.length == 0)
            throw new IllegalAccessException("no bytes were available to read.");


        ByteArrayOutputStream os = new ByteArrayOutputStream(items.length);

        try{
            for (int i = 0; i < items.length; i++){
                os.write(items[i]);
            }
        } catch (IOException ex){
            throw new IllegalAccessException("could not producte final byte array.");
        }

        return os.toByteArray();
    }

    public int unMarshalInt() throws IllegalAccessException{
        byte[][] items = readItems(MarshalTypes.TYPE_INT,
                IntegerMarshaller.MARSHAL_SIZE);

        if (items == null || items.length == 0)
            throw new IllegalAccessException("no ints were read");

        if (items[0] == null || items[0].length == 0)
            throw new IllegalAccessException("no ints were read");

        return IntegerMarshaller.unmarshal(items[0]);

    }

    public int[] unMarshalIntArray() throws IllegalAccessException{
       byte[][] items = readItems(MarshalTypes.TYPE_INT,
                IntegerMarshaller.MARSHAL_SIZE);

        if (items == null || items.length == 0)
            throw new IllegalAccessException("no integers were available to read.");

        int[] result = new int[items.length];

        for (int i = 0; i < items.length; i++){
            result[i] = IntegerMarshaller.unmarshal(items[i]);
        }

        return result;

    }

    public short unMarshalShort() throws IllegalAccessException{
        byte[][] items = readItems(MarshalTypes.TYPE_SHORT,
                ShortMarshaller.MARSHAL_SIZE);

        if (items == null || items.length == 0)
            throw new IllegalAccessException("no shorts were read");

        if (items[0] == null || items[0].length == 0)
            throw new IllegalAccessException("no shorts were read");

        return ShortMarshaller.unmarshal(items[0]);

    }
    public short[] unMarshalShortArray() throws IllegalAccessException{
       byte[][] items = readItems(MarshalTypes.TYPE_SHORT,
                ShortMarshaller.MARSHAL_SIZE);

        if (items == null || items.length == 0)
            throw new IllegalAccessException("no shorts were available to read.");

        short[] result = new short[items.length];

        for (int i = 0; i < items.length; i++){
            result[i] = ShortMarshaller.unmarshal(items[i]);
        }

        return result;


    }

    public float unMarshalFloat() throws IllegalAccessException{
        byte[][] items = readItems(MarshalTypes.TYPE_FLOAT,
                FloatMarshaller.MARSHAL_SIZE);

        if (items == null || items.length == 0)
            throw new IllegalAccessException("no floats were read");

        if (items[0] == null || items[0].length == 0)
            throw new IllegalAccessException("no floats were read");

        return FloatMarshaller.unmarshal(items[0]);

    }

    public float[] unMarshalFloatArray() throws IllegalAccessException{
       byte[][] items = readItems(MarshalTypes.TYPE_FLOAT,
                FloatMarshaller.MARSHAL_SIZE);

        if (items == null || items.length == 0)
            throw new IllegalAccessException("no floats were available to read.");

        float[] result = new float[items.length];

        for (int i = 0; i < items.length; i++){
            result[i] = FloatMarshaller.unmarshal(items[i]);
        }

        return result;

    }

    public double unMarshalDouble() throws IllegalAccessException{
        byte[][] items = readItems(MarshalTypes.TYPE_DOUBLE,
                DoubleMarshaller.MARSHAL_SIZE);

        if (items == null || items.length == 0)
            throw new IllegalAccessException("no doubles were read");

        if (items[0] == null || items[0].length == 0)
            throw new IllegalAccessException("no doubles were read");

        return DoubleMarshaller.unmarshal(items[0]);

    }
    public double[] unMarshalDoubleArray() throws IllegalAccessException{
       byte[][] items = readItems(MarshalTypes.TYPE_DOUBLE,
                DoubleMarshaller.MARSHAL_SIZE);

        if (items == null || items.length == 0)
            throw new IllegalAccessException("no doubles were available to read.");

        double[] result = new double[items.length];

        for (int i = 0; i < items.length; i++){
            result[i] = DoubleMarshaller.unmarshal(items[i]);
        }

        return result;

    }

    public char unMarshalChar() throws IllegalAccessException{
        byte[][] items = readItems(MarshalTypes.TYPE_CHAR,
                CharMarshaller.MARSHAL_SIZE);

        if (items == null || items.length == 0)
            throw new IllegalAccessException("no chars were read");

        if (items[0] == null || items[0].length == 0)
            throw new IllegalAccessException("no chars were read");

        return CharMarshaller.unmarshal(items[0]);

    }
    
    public char[] unMarshalCharArray() throws IllegalAccessException{

       byte[][] items = readItems(MarshalTypes.TYPE_CHAR,
                CharMarshaller.MARSHAL_SIZE);

        if (items == null || items.length == 0)
            throw new IllegalAccessException("no chars were available to read.");

        char[] result = new char[items.length];

        for (int i = 0; i < items.length; i++){
            result[i] = CharMarshaller.unmarshal(items[i]);
        }

        return result;

    }

    public long unMarshalLong() throws IllegalAccessException{
        byte[][] items = readItems(MarshalTypes.TYPE_LONG,
                LongMarshaller.MARSHAL_SIZE);

        if (items == null || items.length == 0)
            throw new IllegalAccessException("no longs were read");

        if (items[0] == null || items[0].length == 0)
            throw new IllegalAccessException("no longs were read");

        return LongMarshaller.unmarshal(items[0]);

    }

    public long[] unMarshalLongArray() throws IllegalAccessException{
       byte[][] items = readItems(MarshalTypes.TYPE_LONG,
                LongMarshaller.MARSHAL_SIZE);

        if (items == null || items.length == 0)
            throw new IllegalAccessException("no longs were available to read.");

        long[] result = new long[items.length];

        for (int i = 0; i < items.length; i++){
            result[i] = LongMarshaller.unmarshal(items[i]);
        }

        return result;


    }

    public boolean unMarshalBoolean() throws IllegalAccessException{

        byte[][] items = readItems(MarshalTypes.TYPE_BOOLEAN,
                BooleanMarshaller.MARSHAL_SIZE);

        if (items == null || items.length == 0)
            throw new IllegalAccessException("no booleans were read");

        if (items[0] == null || items[0].length == 0)
            throw new IllegalAccessException("no booleans were read");

        return BooleanMarshaller.unmarshal(items[0][0]);


    }

    public boolean[] unMarshalBooleanArray() throws IllegalAccessException{
           byte[][] items = readItems(MarshalTypes.TYPE_BOOLEAN,
                BooleanMarshaller.MARSHAL_SIZE);

        if (items == null || items.length == 0)
            throw new IllegalAccessException("no booleans were available to read.");

        boolean[] result = new boolean[items.length * BooleanMarshaller.MARSHAL_SIZE];

        int pos = 0;
        for (int i = 0; i < items.length; i++){
            for (int j = 0; i < items[i].length; j++){
                result[pos++] = BooleanMarshaller.unmarshal(items[i][j]);
            }
        }

        return result;

    }

    public String unMarshalString() throws IllegalAccessException{
        byte[] item = readItem(MarshalTypes.TYPE_STRING);

        if (item == null)
            throw new IllegalAccessException("no booleans were read");

        return StringMarshaller.unmarshal(item);

    }

}
